#include <16F877A.h>
#fuses HS,NOWDT,NOLVP,PUT
#use delay(crystal=20000000)

// Using hardware I2C on PORTC
#use i2c(master, sda=PIN_C4, scl=PIN_C3, slow)

// PCA9555 Address (A0, A1, A2 tied to GND)
#define PCA_ADDR 0x40

// PCA9555 register map (see datasheet)
#define PCA_OUTPUT0 0x02   // Output port 0 register
#define PCA_OUTPUT1 0x03   // Output port 1 register
#define PCA_CONFIG0 0x06   // Direction config port 0
#define PCA_CONFIG1 0x07   // Direction config port 1


// - TRAFFIC LIGHT BIT DEFINITIONS FOR PORTS A/B OF I/O EXPANDER -

// Port A
#define R1 0x01 // Traffic Light 1 - Red
#define Y1 0x02 // Traffic Light 1 - Yellow
#define G1 0x04 // Traffic Light 1 - Green
#define R2 0x08 // Traffic Light 2 - Red
#define Y2 0x10 // Traffic Light 2 - Yellow
#define G2 0x20 // Traffic Light 2 - Green
#define R3 0x40 // Traffic Light 3 - Red
#define Y3 0x80 // Traffic Light 3 - Yellow

// Port B
#define G3      0x01 // Traffic Light 3 - Green
#define PED_RED 0x02 // Pedestrian Light - Red
#define PED_GRN 0x04 // Pedestrian Light - Green


// - 7-SEGMENT DISPLAY CONTROL -

// Digit enable pins (3x 2 digit multiplexed display)
#define DigEn1 PIN_B0 
#define DigEn2 PIN_B1
#define DigEn3 PIN_B2
#define DigEn4 PIN_B3
// DigEn5/6 moved to PORTC due to analogue behaviours on A4/A5
#define DigEn5 PIN_B4
#define DigEn6 PIN_B5

// 7-segment encoding for common anode seven segment display
const int8 SEG_MAP[10] =
{0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90};


// - GLOBAL STATE VARIABLES -

// Countdown timer (seconds)
volatile int8 countdown = 40;

// Used for 1-second timing from Timer0 ISR
volatile int16 ms_counter = 0;
volatile int1 one_second_flag = 0;

// Display buffers (3 two-digit displays)
int8 disp1 = 40;
int8 disp2 = 40;
int8 disp3 = 40;

// Multiplex index (which digit is active)
int8 mux_index = 0;
volatile int16 mux_slow = 0;

// - I/O EXPANDER INITIALISATION -

void PCA_init(void) {
// Provides 50ms delay to allow system stabilisation post start-up
   delay_ms(50); 

   // Set Port0 as OUTPUT (0 = output)
   i2c_start();
   i2c_write(PCA_ADDR);
   i2c_write(PCA_CONFIG0);
   i2c_write(0x00);
   i2c_stop();

   // Set Port1 as OUTPUT
   i2c_start();
   i2c_write(PCA_ADDR);
   i2c_write(PCA_CONFIG1);
   i2c_write(0x00);
   i2c_stop();
}

// Writes to both output ports in one action
void PCA_write_ports(int8 port0, int8 port1) {
   i2c_start();
   i2c_write(PCA_ADDR);
   i2c_write(PCA_OUTPUT0);
   i2c_write(port0); // Port A equivalent
   i2c_write(port1); // Port B equivalent
   i2c_stop();
}


// - TIMER0/1 INTERRUPTS (DISPLAY + TIMING) -

#int_TIMER0 // DISPLAY
void TIMER0_isr(void) {

// the mux_slow function exists to slow down the rate of the multiplexing
// forcing the multiplexing to only run once every so many intervals
// or 1 over X of the regular speed

   mux_slow++; 

   if (mux_slow >= 4){ // sets the output rate to 1/4 of regular speed
      mux_slow = 0;

      // Disables all digits on start-up to prevent ghosting
      output_high(DigEn1);
      output_high(DigEn2);
      output_high(DigEn3);
      output_high(DigEn4);
      output_high(DigEn5);
      output_high(DigEn6);
   
      // Clear segments before writing new value
      output_d(0xFF);
   
      // Multiplexing: display one digit per interrupt
      switch(mux_index) {
         case 0: 
            output_d(SEG_MAP[disp1 % 10]); //Outputs the units value of segmap        
            output_low(DigEn1);// onto the DigEn pins that line up with the 
            output_low(DigEn3);// units value on the 7-segment displays
            output_low(DigEn5);
            break; // units
         case 1: 
            output_d(SEG_MAP[disp1 / 10]); //Outputs the tens value of the 
            output_low(DigEn2);// segmap onto the DigEn Pins that line up with
            output_low(DigEn4);// the tens value on the 7-segment displays
            output_low(DigEn6); 
            break; // tens
            
            
         //case 2: output_d(SEG_MAP[disp2 % 10]); output_low(DigEn3); break;
         //case 3: output_d(SEG_MAP[disp2 / 10]); output_low(DigEn4); break;
         //case 4: output_d(SEG_MAP[disp3 % 10]); output_low(DigEn5); break;
         //case 5: output_d(SEG_MAP[disp3 / 10]); output_low(DigEn6); break;
      }
      delay_us(50);

   // Advance to next digit (wrap around 0-2)
   mux_index = (mux_index + 1) % 2;
   
   }
   
   // Reload timer for consistent interrupt period
   set_timer0(275);

}

// Timer1 ISR Setup

#int_TIMER1 // COUNTDOWN TIMING
void TIMER1_isr(void) {

   set_timer1(64911); // preload for 1ms tick

   ms_counter++;

   if(ms_counter >= 1000) {
      ms_counter = 0;
      one_second_flag = 1;
   }
}


// - TRAFFIC LIGHT CONTROL -

// Each entry represents the full system state for a given countdown value
// This removes any conditional logic and replaces it with direct lookup

typedef struct {
   int8 portA; // Road lights
   int8 portB; // Road 3 + pedestrian lights
} state_t;

// Lookup table (index = countdown value)
// This defines the ENTIRE behaviour of the system
const state_t state_table[41] = {
/*0*/  {Y1 | Y2 | R3, PED_RED},
/*1*/  {Y1 | Y2 | R3, PED_RED},
/*2*/  {R1 | R2 | R3, PED_RED},
/*3*/  {R1 | R2 | R3, PED_RED},
/*4*/  {R1 | R2 | R3, PED_GRN},
/*5*/  {R1 | R2 | R3, PED_GRN},
/*6*/  {R1 | R2 | R3, PED_GRN},
/*7*/  {R1 | R2 | R3, PED_GRN},
/*8*/  {R1 | R2 | R3, PED_GRN},
/*9*/  {R1 | R2 | R3, PED_GRN},
/*10*/ {R1 | R2 | R3, PED_GRN},
/*11*/ {R1 | R2 | R3, PED_RED},
/*12*/ {R1 | R2 | Y3, PED_RED},
/*13*/ {R1 | R2 | Y3, PED_RED},
/*14*/ {R1 | R2,      PED_RED | G3}, //G3 = Port B, moved for legibility
/*15*/ {R1 | R2,      PED_RED | G3},
/*16*/ {R1 | R2,      PED_RED | G3},
/*17*/ {R1 | R2,      PED_RED | G3},
/*18*/ {R1 | R2,      PED_RED | G3},
/*19*/ {R1 | R2,      PED_RED | G3},
/*20*/ {R1 | R2 | Y3, PED_RED},
/*21*/ {R1 | R2 | R3, PED_RED},
/*22*/ {R1 | R2 | R3, PED_RED},
/*23*/ {Y1 | Y2 | R3, PED_RED},
/*24*/ {Y1 | Y2 | R3, PED_RED},
/*25*/ {Y1 | Y2 | R3, PED_RED},
/*26*/ {G1 | G2 | R3, PED_RED},
/*27*/ {G1 | G2 | R3, PED_RED},
/*28*/ {G1 | G2 | R3, PED_RED},
/*29*/ {G1 | G2 | R3, PED_RED},
/*30*/ {G1 | G2 | R3, PED_RED},
/*31*/ {G1 | G2 | R3, PED_RED},
/*32*/ {G1 | G2 | R3, PED_RED},
/*33*/ {G1 | G2 | R3, PED_RED},
/*34*/ {G1 | G2 | R3, PED_RED},
/*35*/ {G1 | G2 | R3, PED_RED},
/*36*/ {G1 | G2 | R3, PED_RED},
/*37*/ {G1 | G2 | R3, PED_RED},
/*38*/ {G1 | G2 | R3, PED_RED},
/*39*/ {G1 | G2 | R3, PED_RED},
/*40*/ {G1 | G2 | R3, PED_RED}
};

// Constant-time state selection
void set_lights(int8 t, int8 *portA, int8 *portB) {
   // Sets bounds to ensure the counter does not go ary
   if(t > 40) t = 40;
   if(t < 0)  t = 0;

   // Direct lookup tables for ports A and B of the I/O Expander
   *portA = state_table[t].portA;
   *portB = state_table[t].portB;
}


// - MAIN LOOP -

void main(void) {

   setup_adc_ports(NO_ANALOGS); // All PORTA pins as digital

   set_tris_d(0x00); // PORTD = output (7-seg segments)
   set_tris_c(0b00011000); // RC6/7 outputs for digits
   set_tris_b(0x00); // PORTB = output (digit enables)
   port_b_pullups(FALSE); // Disabled PORTB internal Pull-ups

   // Timer0 setup (used for display refresh + timing)
   setup_timer_0(RTCC_INTERNAL | RTCC_DIV_256);
   set_timer0(250);

   setup_timer_1(T1_INTERNAL | T1_DIV_BY_8);
   set_timer1(64911);

   PCA_init();   // Initialise I/O expander

   enable_interrupts(INT_TIMER0);
   enable_interrupts(INT_TIMER1);
   enable_interrupts(GLOBAL);

   while(TRUE) {

      // Execute once per second
      if(one_second_flag) {

         one_second_flag = 0;

         // Countdown logic (wraps back to 40)
         if(countdown > 0)
            countdown--;
         else
            countdown = 40;

         // Update all displays with the current countdown value
         disp1 = countdown;
         disp2 = countdown;
         disp3 = countdown;

         int8 portA, portB;

         // Determine traffic light state (constant time)
         set_lights(countdown, &portA, &portB);

         // Output to hardware via I2C expander
         PCA_write_ports(portA, portB);
      }
   }
}
